Dashboard.tsx ➔ Dashboard   F
last analyzed

Complexity

Conditions 20

Size

Total Lines 173
Code Lines 137

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 137
dl 0
loc 173
rs 0
c 0
b 0
f 0
cc 20

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like Dashboard.tsx ➔ Dashboard often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import React, { useEffect, useState } from 'react';
2
import { fetchAqicn } from '../api';
3
import ChartComp from './ChartComp';
4
import MapComp from './MapComp';
5
import './Dashboard.css';
6
7
type Pollutant = { dt: number; aqi: number };
8
type Coords = { lat: number; lon: number };
9
10
export default function Dashboard() {
11
  const [coords, setCoords] = useState<Coords | null>(null);
12
  const [error, setError] = useState<string | null>(null);
13
14
  // API data state
15
  const [aq, setAq] = useState<any>(null);
16
  const [histData, setHistData] = useState<Pollutant[]>([]);
17
  const [forecastData, setForecastData] = useState<Pollutant[]>([]);
18
19
  // Ask for GPS once on mount
20
  useEffect(() => {
21
    if (!navigator.geolocation) {
22
      setError('Geolocation is not supported by this browser.');
23
      return;
24
    }
25
    navigator.geolocation.getCurrentPosition(
26
      ({ coords: { latitude, longitude } }) => {
27
        setCoords({ lat: latitude, lon: longitude });
28
      },
29
      (err) => {
30
        let msg: string;
31
        switch (err.code) {
32
          case err.PERMISSION_DENIED:
33
            msg = 'Permission Denied. Please allow location access in your browser settings.';
34
            break;
35
          case err.POSITION_UNAVAILABLE:
36
            msg = 'Location Information is Unavailable.';
37
            break;
38
          case err.TIMEOUT:
39
            msg = 'The request to get your location timed out.';
40
            break;
41
          default:
42
            msg = 'An Unknown Error Occurred.';
43
        }
44
        console.error('Geolocation Error', err.code, err.message);
45
        setError(msg);
46
      }
47
    );
48
  }, []);
49
50
  // Fetch AQICN data
51
  useEffect(() => {
52
    if (!coords) return;
53
54
    const { lat, lon } = coords;
55
    fetchAqicn(lat, lon).then((data) => {
56
      setAq(data);
57
58
      // Prepare forecast (next 4 days) from AQICN
59
      if (data.data.forecast?.daily?.pm25) {
60
        const forecast = data.data.forecast.daily.pm25.map((d: any) => ({
61
          dt: new Date(d.day).getTime() / 1000,
62
          aqi: d.avg
63
        }));
64
        setForecastData(forecast);
65
      }
66
67
      // Prepare historical (last 24h) if available
68
      // AQICN free API doesn't always give hourly history directly — if not available, fallback to past 1 day daily
69
      if (data.data.forecast?.hourly?.pm25) {
70
        const history = data.data.forecast.hourly.pm25.map((d: any) => ({
71
          dt: new Date(d.time).getTime() / 1000,
72
          aqi: d.avg
73
        }));
74
        setHistData(history);
75
      } else {
76
        // fallback: repeat today's daily avg as a placeholder
77
        if (data.data.forecast?.daily?.pm25?.length > 0) {
78
          const today = data.data.forecast.daily.pm25[0];
79
          setHistData([
80
            { dt: Date.now() / 1000 - 86400, aqi: today.avg },
81
            { dt: Date.now() / 1000, aqi: today.avg }
82
          ]);
83
        }
84
      }
85
    });
86
  }, [coords]);
87
88
  // Manual-entry + waiting state
89
  if (!coords) {
90
    return (
91
      <div className="dashboard">
92
        <p>{error || 'Waiting For Location…'}</p>
93
        <button
94
          onClick={async () => {
95
            const input = window.prompt(
96
              'Enter location as "lat,lon" or Place Name (e.g. "Kuala Lumpur")'
97
            );
98
            if (!input) return;
99
            const parts = input.split(',').map(s => s.trim());
100
            if (parts.length === 2 && !isNaN(+parts[0]) && !isNaN(+parts[1])) {
101
              setCoords({ lat: +parts[0], lon: +parts[1] });
102
              setError(null);
103
              return;
104
            }
105
            try {
106
              const resp = await fetch(
107
                `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(input)}&format=json&limit=1`
108
              );
109
              const results = await resp.json();
110
              if (results.length === 0) {
111
                setError('Location Not Found. Try Another Name.');
112
              } else {
113
                setCoords({
114
                  lat: parseFloat(results[0].lat),
115
                  lon: parseFloat(results[0].lon),
116
                });
117
                setError(null);
118
              }
119
            } catch (e: any) {
120
              console.error('Geocoding Error', e);
121
              setError('Geocoding Failed. Please Try Again.');
122
            }
123
          }}
124
        >
125
          Enter Location Manually
126
        </button>
127
      </div>
128
    );
129
  }
130
131
  if (!aq) {
132
    return <div className="dashboard">Loading Air Quality…</div>;
133
  }
134
135
  const sources = [
136
    {
137
      title: 'AQICN',
138
      aqi: aq.data.aqi,
139
      comps: Object.fromEntries(
140
        Object.entries(aq.data.iaqi).map(([k, v]: any) => [k, v.v])
141
      ),
142
      time: new Date(aq.data.time.iso).toLocaleString(),
143
    }
144
  ];
145
146
  return (
147
    <div className="dashboard">
148
      <h1>AirMerge</h1>
149
150
      <div className="grid">
151
        {sources.map(({ title, aqi, comps, time }) => (
152
          <div
153
            key={title}
154
            className="card"
155
            style={{ borderLeft: `6px solid ${aqColor(aqi)}` }}
156
          >
157
            <h2>{title}</h2>
158
            <p className="aqi">AQI: {aqi}</p>
159
            <p>Time: {time}</p>
160
            <ul className="components">
161
              {Object.entries(comps).map(([pollutant, value]) => (
162
                <li key={pollutant}>
163
                  <span>{pollutant.toUpperCase()}</span>
164
                  <span>{(value as number).toFixed(1)}</span>
165
                </li>
166
              ))}
167
            </ul>
168
          </div>
169
        ))}
170
      </div>
171
172
      <h2>📊 Historical (24h) & Forecast (4d) — AQICN</h2>
173
      <div className="chart-container">
174
        <ChartComp hist={histData} fore={forecastData} />
175
      </div>
176
177
      <h2>🗺️ Map Overlay</h2>
178
      <div className="map-container">
179
        <MapComp lat={coords.lat} lon={coords.lon} />
180
      </div>
181
    </div>
182
  );
183
}
184
185
function aqColor(aqi: number) {
186
  if (aqi <= 50) return '#009966';
187
  if (aqi <= 100) return '#ffde33';
188
  if (aqi <= 150) return '#ff9933';
189
  if (aqi <= 200) return '#cc0033';
190
  if (aqi <= 300) return '#660099';
191
  return '#7e0023';
192
}
193